// ==UserScript== // @name Iwara Download Tool // @description Download videos from iwara.tv // @name:ja Iwara バッチダウンローダー // @description:ja Iwara 動画バッチをダウンロード // @name:zh-CN Iwara 批量下载工具 // @description:zh-CN 批量下载 Iwara 视频 // @icon https://www.iwara.tv/logo.png // @namespace https://github.com/dawn-lc/ // @author dawn-lc // @license Apache-2.0 // @copyright 2024, Dawnlc (https://dawnlc.me/) // @source https://github.com/dawn-lc/IwaraDownloadTool // @supportURL https://github.com/dawn-lc/IwaraDownloadTool/issues // @connect iwara.tv // @connect *.iwara.tv // @connect mmdfans.net // @connect *.mmdfans.net // @connect localhost // @connect 127.0.0.1 // @connect * // @match *://*.iwara.tv/* // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_addStyle // @grant GM_addElement // @grant GM_getResourceText // @grant GM_setClipboard // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_info // @grant GM_getTabs // @grant unsafeWindow // @run-at document-start // @require https://cdn.jsdelivr.net/npm/dexie@3.2.4/dist/dexie.min.js // @require https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js // @require https://cdn.jsdelivr.net/npm/moment@2.30.1/min/moment-with-locales.min.js // @resource toastify-css https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.css // @version 3.2.110 // ==/UserScript== (function () { unsafeWindow.IwaraDownloadTool = true; const originalFetch = unsafeWindow.fetch; const originalPushState = unsafeWindow.history.pushState; const originalReplaceState = unsafeWindow.history.replaceState; const originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild; const originalRemoveChild = unsafeWindow.Node.prototype.removeChild; const originalRemove = unsafeWindow.Element.prototype.remove; const originalAddEventListener = unsafeWindow.EventTarget.prototype.addEventListener; const isNull = (obj) => typeof obj === 'undefined' || obj === null; const isObject = (obj) => !isNull(obj) && typeof obj === 'object' && !Array.isArray(obj); const isString = (obj) => !isNull(obj) && typeof obj === 'string'; const isNumber = (obj) => !isNull(obj) && typeof obj === 'number'; const isElement = (obj) => !isNull(obj) && obj instanceof Element; const isNode = (obj) => !isNull(obj) && obj instanceof Node; const isStringTupleArray = (obj) => Array.isArray(obj) && obj.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string'); const emojiSeq = String.raw `(?:\p{Emoji}\uFE0F\u20E3?|\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation})`; const emojiSTags = String.raw `\u{E0061}-\u{E007A}`; const emojiRegex = new RegExp(String.raw `[\u{1F1E6}-\u{1F1FF}]{2}|\u{1F3F4}[${emojiSTags}]{2}[\u{E0030}-\u{E0039}${emojiSTags}]{1,3}\u{E007F}|${emojiSeq}(?:\u200D${emojiSeq})*`, 'gu'); const hasFunction = (obj, method) => { return !method.isEmpty() && !isNull(obj) ? method in obj && typeof obj[method] === 'function' : false; }; const getString = (obj) => { obj = obj instanceof Error ? String(obj) : obj; obj = obj instanceof Date ? obj.format('YYYY-MM-DD') : obj; return typeof obj === 'object' ? JSON.stringify(obj, null, 2) : String(obj); }; Array.prototype.any = function () { return this.prune().length > 0; }; Array.prototype.prune = function () { return this.filter(i => i !== null && typeof i !== 'undefined'); }; Array.prototype.unique = function (prop) { return this.filter((item, index, self) => index === self.findIndex((t) => (prop ? t[prop] === item[prop] : t === item))); }; Array.prototype.union = function (that, prop) { return [...this, ...that].unique(prop); }; Array.prototype.intersect = function (that, prop) { return this.filter((item) => that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop); }; Array.prototype.difference = function (that, prop) { return this.filter((item) => !that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop); }; Array.prototype.complement = function (that, prop) { return this.union(that, prop).difference(this.intersect(that, prop), prop); }; String.prototype.isEmpty = function () { return !isNull(this) && this.length === 0; }; String.prototype.among = function (start, end, greedy = false) { if (this.isEmpty() || start.isEmpty() || end.isEmpty()) return ''; const startIndex = this.indexOf(start); if (startIndex === -1) return ''; const adjustedStartIndex = startIndex + start.length; const endIndex = greedy ? this.lastIndexOf(end) : this.indexOf(end, adjustedStartIndex); if (endIndex === -1 || endIndex < adjustedStartIndex) return ''; return this.slice(adjustedStartIndex, endIndex); }; String.prototype.splitLimit = function (separator, limit) { if (this.isEmpty() || isNull(separator)) { throw new Error('Empty'); } let body = this.split(separator); return limit ? body.slice(0, limit).concat(body.slice(limit).join(separator)) : body; }; String.prototype.truncate = function (maxLength) { return this.length > maxLength ? this.substring(0, maxLength) : this.toString(); }; String.prototype.trimHead = function (prefix) { return this.startsWith(prefix) ? this.slice(prefix.length) : this.toString(); }; String.prototype.trimTail = function (suffix) { return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this.toString(); }; String.prototype.replaceEmojis = function (replace) { return this.replaceAll(emojiRegex, replace ?? ''); }; String.prototype.toURL = function () { let URLString = this; if (URLString.split('//')[0].isEmpty()) { URLString = `${unsafeWindow.location.protocol}${URLString}`; } return new URL(URLString.toString()); }; Array.prototype.append = function (arr) { this.push(...arr); }; Date.prototype.format = function (format) { return moment(this).locale(language()).format(format); }; String.prototype.replaceVariable = function (replacements, count = 0) { let replaceString = this.toString(); try { replaceString = Object.entries(replacements).reduce((str, [key, value]) => { if (str.includes(`%#${key}:`)) { let format = str.among(`%#${key}:`, '#%').toString(); return str.replaceAll(`%#${key}:${format}#%`, getString(hasFunction(value, 'format') ? value.format(format) : value)); } else { return str.replaceAll(`%#${key}#%`, getString(value)); } }, replaceString); count++; return Object.keys(replacements).map((key) => this.includes(`%#${key}#%`)).includes(true) && count < 128 ? replaceString.replaceVariable(replacements, count) : replaceString; } catch (error) { GM_getValue('isDebug') && console.debug(`replace variable error: ${getString(error)}`); return replaceString; } }; function prune(obj) { if (Array.isArray(obj)) { return obj.filter(isNotEmpty).map(prune); } if (isElement(obj) || isNode(obj)) { return obj; } if (isObject(obj)) { return Object.fromEntries(Object.entries(obj) .filter(([key, value]) => isNotEmpty(value)) .map(([key, value]) => [key, prune(value)])); } return isNotEmpty(obj) ? obj : undefined; } function isNotEmpty(obj) { if (isNull(obj)) { return false; } if (Array.isArray(obj)) { return obj.some(isNotEmpty); } if (isString(obj)) { return !obj.isEmpty(); } if (isNumber(obj)) { return !Number.isNaN(obj); } if (isElement(obj) || isNode(obj)) { return true; } if (isObject(obj)) { return Object.values(obj).some(isNotEmpty); } return true; } const fetch = (input, init, force) => { if (init && init.headers && isStringTupleArray(init.headers)) throw new Error("init headers Error"); if (init && init.method && !(init.method === 'GET' || init.method === 'HEAD' || init.method === 'POST')) throw new Error("init method Error"); return force || (typeof input === 'string' ? input : input.url).toURL().hostname !== unsafeWindow.location.hostname ? new Promise((resolve, reject) => { GM_xmlhttpRequest(prune({ method: (init && init.method) || 'GET', url: typeof input === 'string' ? input : input.url, headers: (init && init.headers) || {}, data: ((init && init.body) || null), onload: function (response) { resolve(new Response(response.responseText, { status: response.status, statusText: response.statusText, })); }, onerror: function (error) { reject(error); } })); }) : originalFetch(input, init); }; const UUID = function () { return Array.from({ length: 8 }, () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)).join(''); }; const ceilDiv = function (dividend, divisor) { return Math.floor(dividend / divisor) + (dividend % divisor > 0 ? 1 : 0); }; const language = function () { let env = (!isNull(config) ? config.language : (navigator.language ?? navigator.languages[0] ?? 'en')).replace('-', '_'); let main = env.split('_').shift() ?? 'en'; return (!isNull(i18n[env]) ? env : !isNull(i18n[main]) ? main : 'en'); }; const renderNode = function (renderCode) { renderCode = prune(renderCode); if (isNull(renderCode)) throw new Error("RenderCode null"); if (typeof renderCode === 'string') { return document.createTextNode(renderCode.replaceVariable(i18n[language()]).toString()); } if (renderCode instanceof Node) { return renderCode; } if (typeof renderCode !== 'object' || !renderCode.nodeType) { throw new Error('Invalid arguments'); } const { nodeType, attributes, events, className, childs } = renderCode; const node = document.createElement(nodeType); (!isNull(attributes) && Object.keys(attributes).any()) && Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value)); (!isNull(events) && Object.keys(events).any()) && Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler)); (!isNull(className) && className.length > 0) && node.classList.add(...[].concat(className)); !isNull(childs) && node.append(...[].concat(childs).map(renderNode)); return node; }; const findElement = function (element, condition) { while (element && !element.matches(condition)) { element = element.parentElement; } return element; }; if (GM_getValue('isDebug')) { console.debug(getString(GM_info)); debugger; } let DownloadType; (function (DownloadType) { DownloadType[DownloadType["Aria2"] = 0] = "Aria2"; DownloadType[DownloadType["IwaraDownloader"] = 1] = "IwaraDownloader"; DownloadType[DownloadType["Browser"] = 2] = "Browser"; DownloadType[DownloadType["Others"] = 3] = "Others"; })(DownloadType || (DownloadType = {})); let PageType; (function (PageType) { PageType["Video"] = "video"; PageType["Image"] = "image"; PageType["VideoList"] = "videoList"; PageType["ImageList"] = "imageList"; PageType["Forum"] = "forum"; PageType["ForumSection"] = "forumSection"; PageType["ForumThread"] = "forumThread"; PageType["Page"] = "page"; PageType["Home"] = "home"; PageType["Profile"] = "profile"; PageType["Subscriptions"] = "subscriptions"; PageType["Playlist"] = "playlist"; PageType["Favorites"] = "favorites"; PageType["Search"] = "search"; PageType["Account"] = "account"; })(PageType || (PageType = {})); let ToastType; (function (ToastType) { ToastType[ToastType["Log"] = 0] = "Log"; ToastType[ToastType["Info"] = 1] = "Info"; ToastType[ToastType["Warn"] = 2] = "Warn"; ToastType[ToastType["Error"] = 3] = "Error"; })(ToastType || (ToastType = {})); let MessageType; (function (MessageType) { MessageType[MessageType["Close"] = 0] = "Close"; MessageType[MessageType["Request"] = 1] = "Request"; MessageType[MessageType["Receive"] = 2] = "Receive"; MessageType[MessageType["Set"] = 3] = "Set"; MessageType[MessageType["Del"] = 4] = "Del"; })(MessageType || (MessageType = {})); let VersionState; (function (VersionState) { VersionState[VersionState["Low"] = 0] = "Low"; VersionState[VersionState["Equal"] = 1] = "Equal"; VersionState[VersionState["High"] = 2] = "High"; })(VersionState || (VersionState = {})); class Version { major; minor; patch; preRelease; buildMetadata; constructor(versionString) { const [version, preRelease, buildMetadata] = versionString.split(/[-+]/); const versionParts = version.split('.').map(Number); this.major = versionParts[0] || 0; this.minor = versionParts.length > 1 ? versionParts[1] : 0; this.patch = versionParts.length > 2 ? versionParts[2] : 0; this.preRelease = preRelease ? preRelease.split('.') : []; this.buildMetadata = buildMetadata; } compare(other) { const compareSegment = (a, b) => { if (a < b) { return VersionState.Low; } else if (a > b) { return VersionState.High; } return VersionState.Equal; }; let state = compareSegment(this.major, other.major); if (state !== VersionState.Equal) return state; state = compareSegment(this.minor, other.minor); if (state !== VersionState.Equal) return state; state = compareSegment(this.patch, other.patch); if (state !== VersionState.Equal) return state; for (let i = 0; i < Math.max(this.preRelease.length, other.preRelease.length); i++) { const pre1 = this.preRelease[i]; const pre2 = other.preRelease[i]; if (pre1 === undefined && pre2 !== undefined) { return VersionState.High; } else if (pre1 !== undefined && pre2 === undefined) { return VersionState.Low; } if (pre1 !== undefined && pre2 !== undefined) { state = compareSegment(isNaN(+pre1) ? pre1 : +pre1, isNaN(+pre2) ? pre2 : +pre2); if (state !== VersionState.Equal) return state; } } return VersionState.Equal; } } class Dictionary extends Map { constructor(data = []) { super(); data.forEach(i => this.set(i[0], i[1])); } toArray() { return Array.from(this); } allKeys() { return Array.from(this.keys()); } allValues() { return Array.from(this.values()); } } class SyncDictionary extends Dictionary { channel; changeTime; id; changeCallback; constructor(id, data = [], changeCallback) { super(data); this.channel = new BroadcastChannel(`${GM_info.script.name}.${id}`); this.changeCallback = changeCallback; this.changeTime = 0; this.id = id; if (isNull(GM_getValue(id, { timestamp: 0, value: [] }).timestamp)) GM_deleteValue(id); originalAddEventListener.call(unsafeWindow, 'beforeunload', this.saveData.bind(this)); originalAddEventListener.call(unsafeWindow, 'pagehide', this.saveData.bind(this)); originalAddEventListener.call(unsafeWindow, 'unload', this.saveData.bind(this)); this.channel.onmessage = (event) => { const message = event.data; const { type, data: { timestamp, value } } = message; GM_getValue('isDebug') && console.debug(`Channel message: ${getString(message)}`); if (timestamp <= this.changeTime) return; switch (type) { case MessageType.Set: value.forEach(item => super.set(item[0], item[1])); break; case MessageType.Del: value.forEach(item => super.delete(item[0])); break; case MessageType.Request: if (this.changeTime === timestamp) return; if (this.changeTime > timestamp) return this.channel.postMessage({ type: MessageType.Receive, data: { timestamp: this.changeTime, value: super.toArray() } }); this.reinitialize(value); break; case MessageType.Close: case MessageType.Receive: if (this.changeTime >= timestamp) return; this.reinitialize(value); break; } this.changeTime = timestamp; this.changeCallback?.(event); }; this.channel.onmessageerror = (event) => { GM_getValue('isDebug') && console.debug(`Channel message error: ${getString(event)}`); }; GM_getTabs((tabs) => { const tabIds = Object.keys(tabs); const isLastTab = tabIds.length <= 1; if (isLastTab) { let save = GM_getValue(id, { timestamp: 0, value: [] }); if (save.timestamp > this.changeTime) { this.changeTime = save.timestamp; this.reinitialize(save.value); } } else { this.channel.postMessage({ type: MessageType.Request, data: { timestamp: this.changeTime, value: super.toArray() } }); } }); } saveData() { const savedData = GM_getValue(this.id, { timestamp: 0, value: [] }); if (this.changeTime > savedData.timestamp) { GM_getTabs((tabs) => { const tabIds = Object.keys(tabs); const isLastTab = tabIds.length <= 1; if (isLastTab) { GM_setValue(this.id, { timestamp: this.changeTime, value: super.toArray() }); } else { this.channel.postMessage({ type: MessageType.Close, data: { timestamp: this.changeTime, value: super.toArray() } }); } }); } } reinitialize(data) { super.clear(); data.forEach(([key, value]) => super.set(key, value)); } set(key, value) { super.set(key, value); this.changeTime = Date.now(); this.channel.postMessage({ type: MessageType.Set, data: { timestamp: this.changeTime, value: [[key, value]] } }); return this; } delete(key) { let isDeleted = super.delete(key); if (isDeleted) { this.changeTime = Date.now(); this.channel.postMessage({ type: MessageType.Del, data: { timestamp: this.changeTime, value: [[key]] } }); } return isDeleted; } } class I18N { zh_CN = this['zh']; zh = { appName: 'Iwara 批量下载工具', language: '语言: ', downloadPriority: '下载画质: ', downloadPath: '下载到: ', downloadProxy: '下载代理: ', aria2Path: 'Aria2 RPC: ', aria2Token: 'Aria2 密钥: ', iwaraDownloaderPath: 'IwaraDownloader RPC: ', iwaraDownloaderToken: 'IwaraDownloader 密钥: ', rename: '重命名', save: '保存', reset: '重置', ok: '确定', on: '开启', off: '关闭', isDebug: '调试模式', downloadType: '下载方式', browserDownload: '浏览器下载', iwaraDownloaderDownload: 'IwaraDownloader下载', autoFollow: '自动关注选中的视频作者', autoLike: '自动点赞选中的视频', addUnlistedAndPrivate: '不公开和私有视频强制显示(需关注作者)', checkDownloadLink: '第三方网盘下载地址检查', checkPriority: '下载画质检查', autoInjectCheckbox: '自动注入选择框', autoCopySaveFileName: '自动复制根据规则生成的文件名', configurationIncompatible: '初始化或配置文件不兼容,请重新配置!', browserDownloadNotEnabled: `未启用下载功能!`, browserDownloadNotWhitelisted: `请求的文件扩展名未列入白名单!`, browserDownloadNotPermitted: `下载功能已启用,但未授予下载权限!`, browserDownloadNotSupported: `目前浏览器/版本不支持下载功能!`, browserDownloadNotSucceeded: `下载未开始或失败!`, browserDownloadUnknownError: `未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!`, browserDownloadTimeout: `下载超时,请检查网络环境是否正常!`, variable: '查看可用变量', downloadTime: '下载时间 ', uploadTime: '发布时间 ', example: '示例: ', result: '结果: ', loadingCompleted: '加载完成', settings: '打开设置', downloadThis: '下载当前视频', manualDownload: '手动下载指定', aria2TaskCheck: 'Aria2任务重启', reverseSelect: '本页反向选中', deselectThis: '取消本页选中', deselectAll: '取消所有选中', selectThis: '本页全部选中', downloadSelected: '下载所选', downloadingSelected: '正在下载所选, 请稍后...', injectCheckbox: '开关选择框', configError: '脚本配置中存在错误,请修改。', alreadyKnowHowToUse: '我已知晓如何使用!!!', notice: [ { nodeType: 'br' }, '添加取消所有选中按钮,点击该按钮将会清空所有选中,请谨慎操作!', { nodeType: 'br' }, '调整下载当前视频功能,默认不再检查第三方下载链接以及是否关注作者和喜欢该视频。' ], useHelpForBase: `请认真阅读使用指南!`, useHelpForInjectCheckbox: `开启“%#autoInjectCheckbox#%”以获得更好的体验!或等待加载出视频卡片后, 点击侧边栏中[%#injectCheckbox#%]开启下载选择框`, useHelpForCheckDownloadLink: '开启“%#checkDownloadLink#%”功能会在下载视频前会检查视频简介以及评论,如果在其中发现疑似第三方网盘下载链接,将会弹出提示,您可以点击提示打开视频页面。', useHelpForManualDownload: [ '使用手动下载功能需要提供视频ID, 如需批量手动下载请提供使用“|”分割的视频ID。', { nodeType: 'br' }, '例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L', { nodeType: 'br' }, '或提供符合以下格式对象的数组json字符串', { nodeType: 'br' }, '{ key: string, value: { Title?: string, Alias?: string, Author?: string } }', { nodeType: 'br' }, '例如: ', { nodeType: 'br' }, '[{ key: "AeGUIRO2D5vQ6F", value: { Title: "237知更鸟", Alias: "骑着牛儿追织女", Author: "user1528210" } },{ key: "qQsUMJa19LcK3L", value: { Title: "Mika Automotive Extradimensional", Alias: "Temptation’s_Symphony", Author: "temptations_symphony" } }]' ], useHelpForBugreport: [ '反馈遇到的BUG、使用问题等请前往: ', { nodeType: 'a', childs: 'Github', attributes: { href: 'https://github.com/dawn-lc/IwaraDownloadTool/' } } ], tryRestartingDownload: '→ 点击此处重新下载 ←', tryReparseDownload: '→ 点击此处重新解析 ←', cdnCacheFinded: '→ 进入 MMD Fans 缓存页面 ←', openVideoLink: '→ 进入视频页面 ←', copySucceed: '复制成功!', pushTaskSucceed: '推送下载任务成功!', connectionTest: '连接测试', settingsCheck: '配置检查', createTask: '创建任务', downloadPathError: '下载路径错误!', browserDownloadModeError: '请启用脚本管理器的浏览器API下载模式!', downloadQualityError: '未找到指定的画质下载地址!', findedDownloadLink: '发现疑似第三方网盘下载地址!', allCompleted: '全部解析完成!', parsing: '预解析中...', parsingProgress: '解析进度: ', manualDownloadTips: '单独下载请直接在此处输入视频ID, 批量下载请提供使用“|”分割的视频ID, 例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L\r\n或提供符合以下格式对象的数组json字符串\r\n{ key: string, value: { Title?: string, Alias?: string, Author?: string } }\r\n例如: \r\n[{ key: "AeGUIRO2D5vQ6F", value: { Title: "237知更鸟", Alias: "骑着牛儿追织女", Author: "user1528210" } },{ key: "qQsUMJa19LcK3L", value: { Title: "Mika Automotive Extradimensional", Alias: "Temptation’s_Symphony", Author: "temptations_symphony" } }]', externalVideo: `非本站视频`, noAvailableVideoSource: '没有可供下载的视频源', videoSourceNotAvailable: '视频源地址不可用', getVideoSourceFailed: '获取视频源失败', downloadFailed: '下载失败!', downloadThisFailed: '未找到可供下载的视频!', pushTaskFailed: '推送下载任务失败!', parsingFailed: '视频信息解析失败!', autoFollowFailed: '自动关注视频作者失败!', autoLikeFailed: '自动点赞视频失败!', }; en = { appName: 'Iwara Download Tool', language: 'Language:', downloadPath: 'Download to:', downloadProxy: 'Download proxy:', rename: 'Rename:', save: 'Save', ok: 'OK', on: 'On', off: 'Off', switchDebug: 'Debug mode:', downloadType: 'Download type:', configurationIncompatible: 'An incompatible configuration file was detected, please reconfigure!', browserDownload: 'Browser download', iwaraDownloaderDownload: 'iwaraDownloader download', checkDownloadLink: 'High-quality download link check:', downloadThis: 'Download this video', autoInjectCheckbox: 'Auto inject selection', variable: 'Available variables:', downloadTime: 'Download time ', uploadTime: 'Upload time ', example: 'Example:', result: 'Result:', loadingCompleted: 'Loading completed', settings: 'Open settings', manualDownload: 'Manual download', reverseSelect: 'Reverse select', deselect: 'Deselect', selectAll: 'Select all', downloadSelected: 'Download selected', downloadingSelected: 'Downloading selected, please wait...', injectCheckbox: 'Switch selection', configError: 'There is an error in the script configuration, please modify it.', alreadyKnowHowToUse: 'I\'m already aware of how to use it!!!', useHelpForInjectCheckbox: "After the video card is loaded, click [%#injectCheckbox#%] in the sidebar to enable the download checkbox", useHelpForCheckDownloadLink: "Before downloading the video, the video introduction and comments will be checked. If a suspected third-party download link is found in them, a prompt will pop up. You can click the prompt to open the video page.", useHelpForManualDownload: "Manual download requires you to provide a video ID! \r\nIf you need to batch download, please use '|' to separate IDs. For example:A|B|C...", useHelpForBugreport: [ 'Report bugs: ', { nodeType: 'a', childs: 'Guthub', attributes: { href: 'https://github.com/dawn-lc/IwaraDownloadTool/issues/new/choose' } } ], downloadFailed: 'Download failed!', tryRestartingDownload: '→ Click here to restrat ←', tryReparseDownload: '→ Click here to reparse ←', openVideoLink: '→ Enter video page ←', pushTaskFailed: 'Failed to push download task!', pushTaskSucceed: 'Pushed download task successfully!', connectionTest: 'Connection test', settingsCheck: 'Configuration check', parsingFailed: 'Video information parsing failed!', createTask: 'Create task', downloadPathError: 'Download path error!', browserDownloadModeError: "Please enable the browser API download mode of the script manager!", downloadQualityError: "No original painting download address!", findedDownloadLink: "Found suspected high-quality download link!", allCompleted: "All parsing completed!", parsingProgress: "Parsing progress:", manualDownloadTips: "Please enter the video ID you want to download! \r\nIf you need to batch download, please use '|' to separate IDs. For example:A|B|C...", externalVideo: `Non-site video`, getVideoSourceFailed: `Failed to get video source`, noAvailableVideoSource: `No available video source`, videoSourceNotAvailable: `Video source address not available`, }; } class Config { configChange; language; autoFollow; autoLike; addUnlistedAndPrivate; autoInjectCheckbox; autoCopySaveFileName; checkDownloadLink; checkPriority; downloadPriority; downloadType; downloadPath; downloadProxy; aria2Path; aria2Token; iwaraDownloaderPath; iwaraDownloaderToken; authorization; priority; constructor() { this.language = language(); this.autoFollow = false; this.autoLike = false; this.autoCopySaveFileName = false; this.autoInjectCheckbox = true; this.checkDownloadLink = true; this.checkPriority = true; this.addUnlistedAndPrivate = true; this.downloadPriority = 'Source'; this.downloadType = DownloadType.Others; this.downloadPath = '/Iwara/%#AUTHOR#%/%#TITLE#%[%#ID#%].mp4'; this.downloadProxy = ''; this.aria2Path = 'http://127.0.0.1:6800/jsonrpc'; this.aria2Token = ''; this.iwaraDownloaderPath = 'http://127.0.0.1:6800/jsonrpc'; this.iwaraDownloaderToken = ''; this.priority = { 'Source': 100, '540': 99, '360': 98, 'preview': 1 }; let body = new Proxy(this, { get: function (target, property) { if (property === 'configChange') { return target.configChange; } let value = GM_getValue(property, target[property]); GM_getValue('isDebug') && console.debug(`get: ${property} ${getString(value)}`); return value; }, set: function (target, property, value) { if (property === 'configChange') { target.configChange = value; return true; } GM_setValue(property, value); GM_getValue('isDebug') && console.debug(`set: ${property} ${getString(value)}`); target.configChange(property); return true; } }); GM_listValues().forEach((value) => { GM_addValueChangeListener(value, (name, old_value, new_value, remote) => { GM_getValue('isDebug') && console.debug(`$Is Remote: ${remote} Change Value: ${name}`); if (remote && !isNull(body.configChange)) body.configChange(name); }); }); return body; } async check() { if (await localPathCheck()) { switch (this.downloadType) { case DownloadType.Aria2: return await aria2Check(); case DownloadType.IwaraDownloader: return await iwaraDownloaderCheck(); case DownloadType.Browser: return await EnvCheck(); default: break; } return true; } else { return false; } } } class configEdit { source; target; interface; interfacePage; constructor(config) { this.target = config; this.target.configChange = (item) => { this.configChange.call(this, item); }; this.interfacePage = renderNode({ nodeType: 'p' }); let save = renderNode({ nodeType: 'button', childs: '%#save#%', attributes: { title: i18n[language()].save }, events: { click: async () => { save.disabled = !save.disabled; if (await this.target.check()) { unsafeWindow.location.reload(); } save.disabled = !save.disabled; } } }); let reset = renderNode({ nodeType: 'button', childs: '%#reset#%', attributes: { title: i18n[language()].reset }, events: { click: () => { firstRun(); unsafeWindow.location.reload(); } } }); this.interface = renderNode({ nodeType: 'div', attributes: { id: 'pluginConfig' }, childs: [ { nodeType: 'div', className: 'main', childs: [ { nodeType: 'h2', childs: '%#appName#%' }, { nodeType: 'label', childs: [ '%#language#% ', { nodeType: 'input', className: 'inputRadioLine', attributes: Object.assign({ name: 'language', type: 'text', value: this.target.language }), events: { change: (event) => { this.target.language = event.target.value; } } } ] }, this.downloadTypeSelect(), this.interfacePage, this.switchButton('checkPriority'), this.switchButton('checkDownloadLink'), this.switchButton('autoFollow'), this.switchButton('autoLike'), this.switchButton('autoInjectCheckbox'), this.switchButton('autoCopySaveFileName'), this.switchButton('addUnlistedAndPrivate'), this.switchButton('isDebug', GM_getValue, (name, e) => { GM_setValue(name, e.target.checked); }, false), ] }, { nodeType: 'p', className: 'buttonList', childs: [ reset, save ] } ] }); } switchButton(name, get, set, defaultValue) { let button = renderNode({ nodeType: 'p', className: 'inputRadioLine', childs: [ { nodeType: 'label', childs: `%#${name}#%`, attributes: { for: name } }, { nodeType: 'input', className: 'switch', attributes: { type: 'checkbox', name: name, }, events: { change: (e) => { if (set !== undefined) { set(name, e); return; } else { this.target[name] = e.target.checked; } } } } ] }); button.querySelector(`[name='${name}']`).checked = get !== undefined ? get(name, defaultValue) : this.target[name] ?? defaultValue ?? false; return button; } inputComponent(name, type, get, set) { return { nodeType: 'label', childs: [ `%#${name}#% `, { nodeType: 'input', attributes: Object.assign({ name: name, type: type ?? 'text', value: get !== undefined ? get(name) : this.target[name] }), events: { change: (e) => { if (set !== undefined) { set(name, e); return; } else { this.target[name] = e.target.value; } } } } ] }; } downloadTypeSelect() { let select = renderNode({ nodeType: 'p', className: 'inputRadioLine', childs: [ `%#downloadType#%`, { nodeType: 'select', childs: Object.keys(DownloadType).filter((i) => isNaN(Number(i))).map((i) => renderNode({ nodeType: 'option', childs: i })), attributes: { name: 'downloadType' }, events: { change: (e) => { this.target.downloadType = e.target.selectedIndex; } } } ] }); select.selectedIndex = Number(this.target.downloadType); return select; } configChange(item) { switch (item) { case 'downloadType': this.interface.querySelector(`[name=${item}]`).selectedIndex = Number(this.target.downloadType); this.pageChange(); break; case 'checkPriority': this.pageChange(); break; default: let element = this.interface.querySelector(`[name=${item}]`); if (element) { switch (element.type) { case 'radio': element.value = this.target[item]; break; case 'checkbox': element.checked = this.target[item]; break; case 'text': case 'password': element.value = this.target[item]; break; default: break; } } break; } } pageChange() { while (this.interfacePage.hasChildNodes()) { this.interfacePage.removeChild(this.interfacePage.firstChild); } let variableInfo = renderNode({ nodeType: 'a', childs: '%#variable#%', attributes: { href: 'https://github.com/dawn-lc/IwaraDownloadTool#路径可用变量' } }); let downloadConfigInput = [ variableInfo, renderNode(this.inputComponent('downloadPath')), renderNode(this.inputComponent('downloadProxy')) ]; let aria2ConfigInput = [ renderNode(this.inputComponent('aria2Path')), renderNode(this.inputComponent('aria2Token', 'password')) ]; let iwaraDownloaderConfigInput = [ renderNode(this.inputComponent('iwaraDownloaderPath')), renderNode(this.inputComponent('iwaraDownloaderToken', 'password')) ]; let BrowserConfigInput = [ variableInfo, renderNode(this.inputComponent('downloadPath')) ]; switch (this.target.downloadType) { case DownloadType.Aria2: downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i)); aria2ConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i)); break; case DownloadType.IwaraDownloader: downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i)); iwaraDownloaderConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i)); break; default: BrowserConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i)); break; } if (this.target.checkPriority) { originalNodeAppendChild.call(this.interfacePage, renderNode(this.inputComponent('downloadPriority'))); } } inject() { if (!unsafeWindow.document.querySelector('#pluginConfig')) { originalNodeAppendChild.call(unsafeWindow.document.body, this.interface); this.configChange('downloadType'); } } } class VideoInfo { ID; UploadTime; Title; FileName; Size; Tags; Liked; Following; Friend; Alias; Author; AuthorID; Private; Unlisted; DownloadQuality; External; ExternalUrl; State; Comments; DownloadUrl; RAW; constructor(info) { if (!isNull(info)) { if (!isNull(info.Title) && !info.Title.isEmpty()) this.Title = info.Title; if (!isNull(info.Alias) && !info.Alias.isEmpty()) this.Alias = info.Alias; if (!isNull(info.Author) && !info.Author.isEmpty()) this.Author = info.Author; } return this; } async init(ID, InfoSource) { try { this.ID = ID; if (isNull(InfoSource)) { config.authorization = `Bearer ${await refreshToken()}`; } else { this.RAW = InfoSource; await db.videos.put(this); } let VideoInfoSource = InfoSource ?? await (await fetch(`https://api.iwara.tv/video/${this.ID}`, { headers: await getAuth() })).json(); if (VideoInfoSource.id === undefined) { let cache = await db.videos.where('ID').equals(this.ID).toArray(); if (cache.any()) { Object.assign(this, cache.pop()); } let cdnCache = await db.caches.where('ID').equals(this.ID).toArray(); if (!cdnCache.any()) { let query = prune({ author: this.Alias ?? this.Author, title: this.Title }); for (const key in query) { let dom = new DOMParser().parseFromString(await (await fetch(`https://mmdfans.net/?query=${encodeURIComponent(`${key}:${query[key]}`)}`)).text(), "text/html"); for (let i of [...dom.querySelectorAll('.mdui-col > a')]) { let imgID = i.querySelector('.mdui-grid-tile > img')?.src?.toURL()?.pathname?.split('/')?.pop()?.trimTail('.jpg'); await db.caches.put({ ID: imgID, href: `https://mmdfans.net${i.getAttribute('href')}` }); } } } cdnCache = await db.caches.where('ID').equals(this.ID).toArray(); if (cdnCache.any()) { let toast = newToast(ToastType.Warn, { node: toastNode([ `${this.Title}[${this.ID}] %#parsingFailed#%`, { nodeType: 'br' }, `%#cdnCacheFinded#%` ], '%#createTask#%'), onClick() { GM_openInTab(cdnCache.pop().href, { active: false, insert: true, setParent: true }); toast.hideToast(); }, }); toast.showToast(); let button = getSelectButton(this.ID); button && button.checked && button.click(); selectList.delete(this.ID); this.State = false; return this; } throw new Error(`${i18n[language()].parsingFailed.toString()} ${VideoInfoSource.message}`); } this.ID = VideoInfoSource.id; this.Title = VideoInfoSource.title ?? this.Title; this.External = !isNull(VideoInfoSource.embedUrl) && !VideoInfoSource.embedUrl.isEmpty(); this.AuthorID = VideoInfoSource.user.id; this.Following = VideoInfoSource.user.following; this.Liked = VideoInfoSource.liked; this.Friend = VideoInfoSource.user.friend; this.Private = VideoInfoSource.private; this.Unlisted = VideoInfoSource.unlisted; this.Alias = VideoInfoSource.user.name; this.Author = VideoInfoSource.user.username; this.UploadTime = new Date(VideoInfoSource.createdAt); this.Tags = VideoInfoSource.tags; this.Comments = `${VideoInfoSource.body}\n`; this.ExternalUrl = VideoInfoSource.embedUrl; await db.videos.put(this); if (!isNull(InfoSource)) { return this; } if (this.External) { throw new Error(i18n[language()].externalVideo.toString()); } const getCommentData = async (commentID = null, page = 0) => { return await (await fetch(`https://api.iwara.tv/video/${this.ID}/comments?page=${page}${!isNull(commentID) && !commentID.isEmpty() ? '&parent=' + commentID : ''}`, { headers: await getAuth() })).json(); }; const getCommentDatas = async (commentID = null) => { let comments = []; let base = await getCommentData(commentID); comments.append(base.results); for (let page = 1; page < ceilDiv(base.count, base.limit); page++) { comments.append((await getCommentData(commentID, page)).results); } let replies = []; for (let index = 0; index < comments.length; index++) { const comment = comments[index]; if (comment.numReplies > 0) { replies.append(await getCommentDatas(comment.id)); } } comments.append(replies); return comments.prune(); }; this.Comments += `${(await getCommentDatas()).map(i => i.body).join('\n')}`.normalize('NFKC'); this.FileName = VideoInfoSource.file.name; this.Size = VideoInfoSource.file.size; let VideoFileSource = (await (await fetch(VideoInfoSource.fileUrl, { headers: await getAuth(VideoInfoSource.fileUrl) })).json()).sort((a, b) => (!isNull(config.priority[b.name]) ? config.priority[b.name] : 0) - (!isNull(config.priority[a.name]) ? config.priority[a.name] : 0)); if (isNull(VideoFileSource) || !(VideoFileSource instanceof Array) || VideoFileSource.length < 1) { throw new Error(i18n[language()].getVideoSourceFailed.toString()); } this.DownloadQuality = config.checkPriority ? config.downloadPriority : VideoFileSource[0].name; let fileList = VideoFileSource.filter(x => x.name === this.DownloadQuality); if (!fileList.any()) throw new Error(i18n[language()].noAvailableVideoSource.toString()); let Source = fileList[Math.floor(Math.random() * fileList.length)].src.download; if (isNull(Source) || Source.isEmpty()) throw new Error(i18n[language()].videoSourceNotAvailable.toString()); this.DownloadUrl = decodeURIComponent(`https:${Source}`); this.State = true; await db.videos.put(this); return this; } catch (error) { let data = this; let toast = newToast(ToastType.Error, { node: toastNode([ `${this.Title}[${this.ID}] %#parsingFailed#%`, { nodeType: 'br' }, `${getString(error)}`, { nodeType: 'br' }, this.External ? `%#openVideoLink#%` : `%#tryReparseDownload#%` ], '%#createTask#%'), async onClick() { toast.hideToast(); if (data.External) { GM_openInTab(data.ExternalUrl, { active: false, insert: true, setParent: true }); } else { pushDownloadTask(await new VideoInfo(data).init(data.ID)); } }, }); toast.showToast(); let button = getSelectButton(this.ID); button && button.checked && button.click(); selectList.delete(this.ID); this.State = false; return this; } } } class Database extends Dexie { videos; caches; aria2Tasks; constructor() { super("VideoDatabase"); this.version(2).stores({ videos: 'ID', caches: 'ID' }); this.videos = this.table("videos"); this.caches = this.table("caches"); } async getFilteredVideos(startTime, endTime) { const allVideos = await this.videos.toArray(); return allVideos.filter(video => { const uploadTime = new Date(video.UploadTime); return (uploadTime >= startTime && uploadTime <= endTime && (video.Private !== false || video.Unlisted !== false)); }); } } class menu { source; interface; interfacePage; constructor() { this.interfacePage = renderNode({ nodeType: 'ul' }); this.interface = renderNode({ nodeType: 'div', attributes: { id: 'pluginMenu' }, childs: this.interfacePage }); } button(name, click) { return renderNode(prune({ nodeType: 'li', childs: `%#${name}#%`, events: { click: (event) => { click(name, event); event.stopPropagation(); return false; } } })); } pageChange(pageType) { while (this.interfacePage.hasChildNodes()) { this.interfacePage.removeChild(this.interfacePage.firstChild); } let manualDownloadButton = this.button('manualDownload', (name, event) => { addDownloadTask(); }); let settingsButton = this.button('settings', (name, event) => { editConfig.inject(); }); let baseButtons = [manualDownloadButton, settingsButton]; let injectCheckboxButton = this.button('injectCheckbox', (name, event) => { if (unsafeWindow.document.querySelector('.selectButton')) { unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => { element.remove(); }); } else { unsafeWindow.document.querySelectorAll(`.videoTeaser`).forEach((element) => { injectCheckbox(element, compatible); }); } }); let deselectAllButton = this.button('deselectAll', (name, event) => { for (const id of selectList.keys()) { let button = getSelectButton(id); if (button && button.checked) button.checked = false; selectList.delete(id); } }); let reverseSelectButton = this.button('reverseSelect', (name, event) => { unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => { element.click(); }); }); let selectThisButton = this.button('selectThis', (name, event) => { unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => { let button = element; !button.checked && button.click(); }); }); let deselectThisButton = this.button('deselectThis', (name, event) => { unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => { let button = element; button.checked && button.click(); }); }); let downloadSelectedButton = this.button('downloadSelected', (name, event) => { analyzeDownloadTask(); newToast(ToastType.Info, { text: `%#${name}#%`, close: true }).showToast(); }); let selectButtons = [injectCheckboxButton, deselectAllButton, reverseSelectButton, selectThisButton, deselectThisButton, downloadSelectedButton]; let downloadThisButton = this.button('downloadThis', async (name, event) => { let ID = unsafeWindow.location.href.toURL().pathname.split('/')[2]; let Title = unsafeWindow.document.querySelector('.page-video__details')?.childNodes[0]?.textContent; let videoInfo = await (new VideoInfo(prune({ Title: Title, }))).init(ID); videoInfo.State && await pushDownloadTask(videoInfo, true); }); let aria2TaskCheckButton = this.button('aria2TaskCheck', (name, event) => { aria2TaskCheck(); }); GM_getValue('isDebug') && originalNodeAppendChild.call(this.interfacePage, aria2TaskCheckButton); switch (pageType) { case PageType.Video: originalNodeAppendChild.call(this.interfacePage, downloadThisButton); selectButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i)); baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i)); break; case PageType.Search: case PageType.Profile: case PageType.Home: case PageType.VideoList: case PageType.Subscriptions: case PageType.Playlist: case PageType.Favorites: case PageType.Account: selectButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i)); baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i)); break; case PageType.Page: case PageType.Forum: case PageType.Image: case PageType.ImageList: case PageType.ForumSection: case PageType.ForumThread: default: baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i)); break; } } inject() { if (!unsafeWindow.document.querySelector('#pluginMenu')) { new MutationObserver((mutationsList) => { for (let mutation of mutationsList) { if (mutation.type !== 'childList' || mutation.addedNodes.length < 1) { continue; } let pages = [...mutation.addedNodes].filter(i => isElement(i)).filter(i => i.classList.contains('page')); if (pages.length < 1) { continue; } if (unsafeWindow.location.pathname.toLowerCase().split('/').pop() === 'search') { this.pageChange(PageType.Search); continue; } let page = pages.find(i => i.classList.length > 1); if (!page) { continue; } this.pageChange(page.classList[1].split('-').pop()); } }).observe(unsafeWindow.document.getElementById('app'), { childList: true, subtree: true }); originalNodeAppendChild.call(unsafeWindow.document.body, this.interface); this.pageChange(PageType.Page); } } } GM_addStyle(GM_getResourceText('toastify-css')); GM_addStyle(`.rainbow-text{background-image:linear-gradient(to right,red,#ff7f00,#ff0,#0f0,#00f,#8b00ff);background-clip:text;-webkit-text-fill-color:transparent;background-size:600% 100%;animation:rainbow .5s infinite linear}@keyframes rainbow{0%{background-position:0 0}100%{background-position:100% 0}}#pluginMenu{z-index:2147483644;color:#fff;position:fixed;top:50%;right:0;padding:10px;background-color:#565656;border:1px solid #ccc;border-radius:5px;box-shadow:0 0 10px #ccc;transform:translate(2%,-50%)}#pluginMenu ul{list-style:none;margin:0;padding:0}#pluginMenu li{padding:5px 10px;cursor:pointer;text-align:center;user-select:none}#pluginMenu li:hover{background-color:#000000cc;border-radius:3px}#pluginConfig{color:var(--text);position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.75);z-index:2147483646;display:flex;flex-direction:column;align-items:center;justify-content:center}#pluginConfig .main{background-color:var(--body);padding:24px;margin:10px;overflow-y:auto;width:400px}#pluginConfig .buttonList{display:flex;flex-direction:row;justify-content:center}@media (max-width:640px){#pluginConfig .main{width:100%}}#pluginConfig button{background-color:#00f;margin:0 20px 0 20px;padding:10px 20px;color:#fff;font-size:18px;border:none;border-radius:4px;cursor:pointer}#pluginConfig button{background-color:#00f}#pluginConfig button[disabled]{background-color:#a9a9a9;cursor:not-allowed}#pluginConfig p{display:flex;flex-direction:column}#pluginConfig p label{display:flex;flex-direction:column;margin:5px 0 5px 0}#pluginConfig .inputRadioLine{display:flex;align-items:center;flex-direction:row;justify-content:space-between}#pluginConfig input[type=password],#pluginConfig input[type=text]{outline:0;border-top:none;border-right:none;border-left:none;border-image:initial;border-bottom:1px solid var(--muted);line-height:1;height:30px;box-sizing:border-box;width:100%;background-color:var(--body);color:var(--text)}#pluginConfig input[type=checkbox].switch{outline:0;appearance:none;-webkit-appearance:none;-moz-appearance:none;position:relative;width:40px;height:20px;background:#ccc;border-radius:10px;transition:border-color .2s,background-color .2s}#pluginConfig input[type=checkbox].switch::after{content:'';display:inline-block;width:40%;height:80%;border-radius:50%;background:#fff;box-shadow:0,0,2px,#999;transition:.2s;top:2px;position:absolute;right:55%}#pluginConfig input[type=checkbox].switch:checked{background:#13ce66}#pluginConfig input[type=checkbox].switch:checked::after{content:'';position:absolute;right:2px;top:2px}#pluginOverlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.75);z-index:2147483645;display:flex;flex-direction:column;align-items:center;justify-content:center}#pluginOverlay .main{color:#fff;font-size:24px;width:60%;background-color:rgba(64,64,64,.75);padding:24px;margin:10px;overflow-y:auto}@media (max-width:640px){#pluginOverlay .main{width:100%}}#pluginOverlay button{padding:10px 20px;color:#fff;font-size:18px;border:none;border-radius:4px;cursor:pointer}#pluginOverlay button{background-color:#00f}#pluginOverlay button[disabled]{background-color:#a9a9a9;cursor:not-allowed}#pluginOverlay .checkbox{width:32px;height:32px;margin:0 4px 0 0;padding:0}#pluginOverlay .checkbox-container{display:flex;align-items:center;margin:0 0 10px 0}#pluginOverlay .checkbox-label{color:#fff;font-size:32px;font-weight:700;margin-left:10px;display:flex;align-items:center}.selectButton{accent-color:rgb(50,110,193);position:absolute;width:38px;height:38px;bottom:24px;right:0;cursor:pointer}.selectButtonCompatible{width:32px;height:32px;bottom:0;right:4px;transform:translate(-50%,-50%);margin:0;padding:0;cursor:pointer}.toastify h3{margin:0 0 10px 0}.toastify p{margin:0}`); var mouseTarget = null; var compatible = navigator.userAgent.toLowerCase().includes('firefox'); var i18n = new I18N(); var config = new Config(); var db = new Database(); var pageSelectButtons = new Dictionary(); if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('3.2.76')) === VersionState.Low) { GM_deleteValue('selectList'); } var selectList = new SyncDictionary('selectList', [], (event) => { const message = event.data; const updateButtonState = (videoID) => { const selectButton = getSelectButton(videoID); if (selectButton) selectButton.checked = selectList.has(videoID); }; switch (message.type) { case MessageType.Set: case MessageType.Del: updateButtonState(message.data.value[0][0]); break; case MessageType.Request: case MessageType.Receive: document.querySelectorAll('input.selectButton').forEach(button => { const videoid = button.getAttribute('videoid'); if (videoid) button.checked = selectList.has(videoid); }); break; default: break; } }); var editConfig = new configEdit(config); var pluginMenu = new menu(); function getSelectButton(id) { return pageSelectButtons.has(id) ? pageSelectButtons.get(id) : unsafeWindow.document.querySelector(`input.selectButton[videoid="${id}"]`); } function getPlayload(authorization) { return JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(authorization.split(' ').pop().split('.')[1])))); } const modifyFetch = async (input, init) => { GM_getValue('isDebug') && console.debug(`Fetch ${input}`); let url = (input instanceof Request ? input.url : input instanceof URL ? input.href : input).toURL(); if (url.hostname.includes('sentry.io')) return undefined; if (!isNull(init) && !isNull(init.headers) && !isStringTupleArray(init.headers)) { let authorization = null; if (init.headers instanceof Headers) { authorization = init.headers.has('Authorization') ? init.headers.get('Authorization') : null; } else { for (const key in init.headers) { if (key.toLowerCase() === "authorization") { authorization = init.headers[key]; break; } } } if (!isNull(authorization) && authorization !== config.authorization) { let playload = getPlayload(authorization); if (playload['type'] === 'refresh_token') { GM_getValue('isDebug') && console.debug(`refresh_token: ${authorization.split(' ').pop()}`); isNull(localStorage.getItem('token')) && localStorage.setItem('token', authorization.split(' ').pop()); } if (playload['type'] === 'access_token') { config.authorization = `Bearer ${authorization.split(' ').pop()}`; GM_getValue('isDebug') && console.debug(JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(config.authorization.split('.')[1]))))); GM_getValue('isDebug') && console.debug(`access_token: ${config.authorization.split(' ').pop()}`); } } } return new Promise((resolve, reject) => originalFetch(input, init) .then(async (response) => { if (url.hostname !== 'api.iwara.tv' || url.pathname.isEmpty()) return resolve(response); let path = url.pathname.toLowerCase().split('/').slice(1); switch (path[0]) { case 'videos': let cloneResponse = response.clone(); if (!cloneResponse.ok) break; let cloneBody = await cloneResponse.json(); let list = cloneBody.results; list.forEach(info => new VideoInfo().init(info.id, info)); if (!config.addUnlistedAndPrivate) return resolve(cloneResponse); if (url.searchParams.has('subscribed') || url.searchParams.has('user')) break; list = list.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); GM_getValue('isDebug') && console.debug(new Date(list.at(0).createdAt), new Date(list.at(-1).createdAt)); let cache = await db.getFilteredVideos(new Date(list.at(0).createdAt), new Date(list.at(-1).createdAt)); if (cache.any()) { cloneBody.count = cloneBody.count + cache.length; cloneBody.limit = cloneBody.limit + cache.length; cloneBody.results.push(...cache.map(i => i.RAW)); cloneBody.results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); } return resolve(new Response(JSON.stringify(cloneBody), { status: cloneResponse.status, statusText: cloneResponse.statusText, headers: Object.fromEntries(cloneResponse.headers.entries()) })); default: break; } return resolve(response); }) .catch((err) => reject(err))); }; unsafeWindow.fetch = modifyFetch; unsafeWindow.EventTarget.prototype.addEventListener = function (type, listener, options) { originalAddEventListener.call(this, type, listener, options); }; async function refreshToken() { let refresh = config.authorization; try { refresh = (await (await fetch(`https://api.iwara.tv/user/token`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } })).json())['accessToken']; } catch (error) { console.warn(`Refresh token error: ${getString(error)}`); } return refresh; } async function getXVersion(urlString) { let url = urlString.toURL(); const data = new TextEncoder().encode(`${url.pathname.split("/").pop()}_${url.searchParams.get('expires')}_5nFp9kmbNnHdAFhaqMvt`); const hashBuffer = await crypto.subtle.digest('SHA-1', data); return Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join(''); } async function getAuth(url) { return Object.assign({ 'Cooike': unsafeWindow.document.cookie, 'Authorization': config.authorization }, !isNull(url) && !url.isEmpty() ? { 'X-Version': await getXVersion(url) } : {}); } async function addDownloadTask() { let textArea = renderNode({ nodeType: "textarea", attributes: { placeholder: i18n[language()].manualDownloadTips, style: 'margin-bottom: 10px;', rows: "16", cols: "96" } }); let body = renderNode({ nodeType: "div", attributes: { id: "pluginOverlay" }, childs: [ textArea, { nodeType: "button", events: { click: (e) => { if (!isNull(textArea.value) && !textArea.value.isEmpty()) { try { let list = JSON.parse(textArea.value); analyzeDownloadTask(new Dictionary(list)); } catch (error) { let IDList = new Dictionary(); textArea.value.split('|').map(ID => IDList.set(ID, {})); analyzeDownloadTask(IDList); } } body.remove(); } }, childs: "确认" } ] }); unsafeWindow.document.body.appendChild(body); } async function analyzeDownloadTask(list = selectList) { let size = list.size; let node = renderNode({ nodeType: 'p', childs: `%#parsingProgress#%[${list.size}/${size}]` }); let start = newToast(ToastType.Info, { node: node, duration: -1 }); start.showToast(); if (GM_getValue('isDebug') && config.downloadType === DownloadType.Aria2) { let completed = (await aria2API('aria2.tellStopped', [0, 2048, [ 'gid', 'status', 'files', 'errorCode', 'bittorrent' ]])).result.filter((task) => isNull(task.bittorrent) && (task.status === 'complete' || task.errorCode === '13')).map((task) => aria2TaskExtractVideoID(task)).filter(Boolean); for (let key of list.allKeys().intersect(completed)) { let button = getSelectButton(key); if (!isNull(button)) button.checked = false; list.delete(key); node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`; } } let infoList = (await Promise.all(list.allKeys().map(async (id) => { let caches = db.videos.where('ID').equals(id); let cache = await caches.first(); if ((await caches.count()) < 1) { let parseToast = newToast(ToastType.Info, { text: `${list.get(id).Title ?? id} %#parsing#%`, duration: -1, close: true, onClick() { parseToast.hideToast(); } }); parseToast.showToast(); cache = await new VideoInfo(list.get(id)).init(id); parseToast.hideToast(); } return cache; }))).sort((a, b) => a.UploadTime.getTime() - b.UploadTime.getTime()); for (let videoInfo of infoList) { let button = getSelectButton(videoInfo.ID); let video = videoInfo.State ? videoInfo : await new VideoInfo(list.get(videoInfo.ID)).init(videoInfo.ID); video.State && await pushDownloadTask(video); if (!isNull(button)) button.checked = false; list.delete(videoInfo.ID); node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`; } start.hideToast(); if (size != 1) { let completed = newToast(ToastType.Info, { text: `%#allCompleted#%`, duration: -1, close: true, onClick() { completed.hideToast(); } }); completed.showToast(); } } function checkIsHaveDownloadLink(comment) { if (!config.checkDownloadLink || isNull(comment) || comment.isEmpty()) { return false; } return [ 'iwara.zip', 'pan.baidu', '/s/', 'mega.nz', 'drive.google.com', 'aliyundrive', 'uploadgig', 'katfile', 'storex', 'subyshare', 'rapidgator', 'filebe', 'filespace', 'mexa.sh', 'mexashare', 'mx-sh.net', 'uploaded.', 'icerbox', 'alfafile', '1drv.ms', 'onedrive.', 'gofile.io', 'workupload.com', 'pixeldrain.', 'gigafile.nu' ].filter(i => comment.toLowerCase().includes(i)).any(); } function toastNode(body, title) { return renderNode({ nodeType: 'div', childs: [ !isNull(title) && !title.isEmpty() ? { nodeType: 'h3', childs: `%#appName#% - ${title}` } : { nodeType: 'h3', childs: '%#appName#%' }, { nodeType: 'p', childs: body } ] }); } function getTextNode(node) { return node.nodeType === Node.TEXT_NODE ? node.textContent || '' : node.nodeType === Node.ELEMENT_NODE ? Array.from(node.childNodes) .map(getTextNode) .join('') : ''; } function newToast(type, params) { const logFunc = { [ToastType.Warn]: console.warn, [ToastType.Error]: console.error, [ToastType.Log]: console.log, [ToastType.Info]: console.info, }[type] || console.log; params = Object.assign({ newWindow: true, gravity: 'top', position: 'left', stopOnFocus: true }, type === ToastType.Warn && { duration: -1, style: { background: 'linear-gradient(-30deg, rgb(119 76 0), rgb(255 165 0))' } }, type === ToastType.Error && { duration: -1, style: { background: 'linear-gradient(-30deg, rgb(108 0 0), rgb(215 0 0))' } }, !isNull(params) && params); if (!isNull(params.text)) { params.text = params.text.replaceVariable(i18n[language()]).toString(); } logFunc((!isNull(params.text) ? params.text : !isNull(params.node) ? getTextNode(params.node) : 'undefined').replaceVariable(i18n[language()])); return Toastify(params); } async function pushDownloadTask(videoInfo, bypass = false) { if (!videoInfo.State) { return; } if (!bypass) { if (config.autoFollow && !videoInfo.Following) { if ((await fetch(`https://api.iwara.tv/user/${videoInfo.AuthorID}/followers`, { method: 'POST', headers: await getAuth() })).status !== 201) newToast(ToastType.Warn, { text: `${videoInfo.Alias} %#autoFollowFailed#%`, close: true }).showToast(); } if (config.autoLike && !videoInfo.Liked) { if ((await fetch(`https://api.iwara.tv/video/${videoInfo.ID}/like`, { method: 'POST', headers: await getAuth() })).status !== 201) newToast(ToastType.Warn, { text: `${videoInfo.Title} %#autoLikeFailed#%`, close: true }).showToast(); } if (config.checkDownloadLink && checkIsHaveDownloadLink(videoInfo.Comments)) { let toastBody = toastNode([ `${videoInfo.Title}[${videoInfo.ID}] %#findedDownloadLink#%`, { nodeType: 'br' }, `%#openVideoLink#%` ], '%#createTask#%'); let toast = newToast(ToastType.Warn, { node: toastBody, close: config.autoCopySaveFileName, onClick() { GM_openInTab(`https://www.iwara.tv/video/${videoInfo.ID}`, { active: false, insert: true, setParent: true }); if (config.autoCopySaveFileName) { GM_setClipboard(analyzeLocalPath(config.downloadPath.replaceVariable({ NowTime: new Date(), UploadTime: videoInfo.UploadTime, AUTHOR: videoInfo.Author, ID: videoInfo.ID, TITLE: videoInfo.Title, ALIAS: videoInfo.Alias, QUALITY: videoInfo.DownloadQuality }).trim()).filename, "text"); toastBody.appendChild(renderNode({ nodeType: 'p', childs: '%#copySucceed#%' })); } else { toast.hideToast(); } } }); toast.showToast(); return; } if (config.checkPriority && videoInfo.DownloadQuality !== config.downloadPriority) { let toast = newToast(ToastType.Warn, { node: toastNode([ `${videoInfo.Title}[${videoInfo.ID}] %#downloadQualityError#%`, { nodeType: 'br' }, `%#tryReparseDownload#%` ], '%#createTask#%'), async onClick() { toast.hideToast(); await pushDownloadTask(await new VideoInfo(videoInfo).init(videoInfo.ID)); } }); toast.showToast(); return; } } switch (config.downloadType) { case DownloadType.Aria2: aria2Download(videoInfo); break; case DownloadType.IwaraDownloader: iwaraDownloaderDownload(videoInfo); break; case DownloadType.Browser: browserDownload(videoInfo); break; default: othersDownload(videoInfo); break; } } function analyzeLocalPath(path) { let matchPath = path.replaceAll('//', '/').replaceAll('\\\\', '/').match(/^([a-zA-Z]:)?[\/\\]?([^\/\\]+[\/\\])*([^\/\\]+\.\w+)$/); if (isNull(matchPath)) throw new Error(`%#downloadPathError#%["${path}"]`); try { return { fullPath: matchPath[0], drive: matchPath[1] || '', filename: matchPath[3] }; } catch (error) { throw new Error(`%#downloadPathError#% ["${matchPath.join(',')}"]`); } } async function EnvCheck() { try { if (GM_info.downloadMode !== 'browser') { GM_getValue('isDebug') && console.debug(GM_info); throw new Error('%#browserDownloadModeError#%'); } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `%#configError#%`, { nodeType: 'br' }, getString(error) ], '%#settingsCheck#%'), position: 'center', onClick() { toast.hideToast(); } }); toast.showToast(); return false; } return true; } async function localPathCheck() { try { let pathTest = analyzeLocalPath(config.downloadPath); for (const key in pathTest) { if (!Object.prototype.hasOwnProperty.call(pathTest, key) || pathTest[key]) { } } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `%#downloadPathError#%`, { nodeType: 'br' }, getString(error) ], '%#settingsCheck#%'), position: 'center', onClick() { toast.hideToast(); } }); toast.showToast(); return false; } return true; } async function aria2Check() { try { let res = await (await fetch(config.aria2Path, { method: 'POST', headers: { 'accept': 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ 'jsonrpc': '2.0', 'method': 'aria2.tellActive', 'id': UUID(), 'params': ['token:' + config.aria2Token] }) })).json(); if (res.error) { throw new Error(res.error.message); } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `Aria2 RPC %#connectionTest#%`, { nodeType: 'br' }, getString(error) ], '%#settingsCheck#%'), position: 'center', onClick() { toast.hideToast(); } }); toast.showToast(); return false; } return true; } async function iwaraDownloaderCheck() { try { let res = await (await fetch(config.iwaraDownloaderPath, { method: 'POST', headers: { 'accept': 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(prune({ 'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)), 'code': 'State', 'token': config.iwaraDownloaderToken })) })).json(); if (res.code !== 0) { throw new Error(res.msg); } } catch (error) { let toast = newToast(ToastType.Error, { node: toastNode([ `IwaraDownloader RPC %#connectionTest#%`, { nodeType: 'br' }, getString(error) ], '%#settingsCheck#%'), position: 'center', onClick() { toast.hideToast(); } }); toast.showToast(); return false; } return true; } function aria2Download(videoInfo) { (async function (id, author, name, uploadTime, info, tag, quality, alias, downloadUrl) { let localPath = analyzeLocalPath(config.downloadPath.replaceVariable({ NowTime: new Date(), UploadTime: uploadTime, AUTHOR: author, ID: id, TITLE: name.normalize('NFKC').replaceAll(/(\P{Mark})(\p{Mark}+)/gu, '_').replaceEmojis('_').replace(/^\.|[\\\\/:*?\"<>|]/img, '_').truncate(72), ALIAS: alias, QUALITY: quality }).trim()); let res = await aria2API('aria2.addUri', [ [downloadUrl], prune({ 'all-proxy': config.downloadProxy, 'out': localPath.filename, 'dir': localPath.fullPath.replace(localPath.filename, ''), 'referer': window.location.hostname, 'header': [ 'Cookie:' + unsafeWindow.document.cookie ] }) ]); console.log(`Aria2 ${name} ${JSON.stringify(res)}`); newToast(ToastType.Info, { node: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`) }).showToast(); }(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl)); } function iwaraDownloaderDownload(videoInfo) { (async function (videoInfo) { let r = await (await fetch(config.iwaraDownloaderPath, { method: 'POST', headers: { 'accept': 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(prune({ 'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)), 'code': 'add', 'token': config.iwaraDownloaderToken, 'data': { 'info': { 'title': videoInfo.Title, 'url': videoInfo.DownloadUrl, 'size': videoInfo.Size, 'source': videoInfo.ID, 'alias': videoInfo.Alias, 'author': videoInfo.Author, 'uploadTime': videoInfo.UploadTime, 'comments': videoInfo.Comments, 'tags': videoInfo.Tags, 'path': config.downloadPath.replaceVariable({ NowTime: new Date(), UploadTime: videoInfo.UploadTime, AUTHOR: videoInfo.Author, ID: videoInfo.ID, TITLE: videoInfo.Title, ALIAS: videoInfo.Alias, QUALITY: videoInfo.DownloadQuality }) }, 'option': { 'proxy': config.downloadProxy, 'cookies': unsafeWindow.document.cookie } } })) })).json(); if (r.code === 0) { console.log(`${videoInfo.Title} %#pushTaskSucceed#% ${r}`); newToast(ToastType.Info, { node: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`) }).showToast(); } else { let toast = newToast(ToastType.Error, { node: toastNode([ `${videoInfo.Title}[${videoInfo.ID}] %#pushTaskFailed#% `, { nodeType: 'br' }, r.msg ], '%#iwaraDownloaderDownload#%'), onClick() { toast.hideToast(); } }); toast.showToast(); } }(videoInfo)); } function othersDownload(videoInfo) { (async function (ID, Author, Name, UploadTime, DownloadQuality, Alias, DownloadUrl) { DownloadUrl.searchParams.set('download', analyzeLocalPath(config.downloadPath.replaceVariable({ NowTime: new Date(), UploadTime: UploadTime, AUTHOR: Author, ID: ID, TITLE: Name.normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_').truncate(72), ALIAS: Alias, QUALITY: DownloadQuality }).trim()).filename); GM_openInTab(DownloadUrl.href, { active: false, insert: true, setParent: true }); }(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl.toURL())); } function browserDownload(videoInfo) { (async function (ID, Author, Name, UploadTime, Info, Tag, DownloadQuality, Alias, DownloadUrl) { function browserDownloadError(error) { let errorInfo = getString(Error); if (!(error instanceof Error)) { errorInfo = { 'not_enabled': `%#browserDownloadNotEnabled#%`, 'not_whitelisted': `%#browserDownloadNotWhitelisted#%`, 'not_permitted': `%#browserDownloadNotPermitted#%`, 'not_supported': `%#browserDownloadNotSupported#%`, 'not_succeeded': `%#browserDownloadNotSucceeded#% ${error.details ?? getString(error.details)}` }[error.error] || `%#browserDownloadUnknownError#%`; } let toast = newToast(ToastType.Error, { node: toastNode([ `${Name}[${ID}] %#downloadFailed#%`, { nodeType: 'br' }, errorInfo, { nodeType: 'br' }, `%#tryRestartingDownload#%` ], '%#browserDownload#%'), async onClick() { toast.hideToast(); await pushDownloadTask(videoInfo); } }); toast.showToast(); } GM_download({ url: DownloadUrl, saveAs: false, name: config.downloadPath.replaceVariable({ NowTime: new Date(), UploadTime: UploadTime, AUTHOR: Author, ID: ID, TITLE: Name.normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_').truncate(72), ALIAS: Alias, QUALITY: DownloadQuality }).trim(), onerror: (err) => browserDownloadError(err), ontimeout: () => browserDownloadError(new Error('%#browserDownloadTimeout#%')) }); }(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl)); } async function aria2API(method, params) { return await (await fetch(config.aria2Path, { headers: { 'accept': 'application/json', 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: method, id: UUID(), params: [`token:${config.aria2Token}`, ...params] }), method: 'POST' })).json(); } function aria2TaskExtractVideoID(task) { if (isNull(task.files)) { GM_getValue('isDebug') && console.debug(`check aria2 task files fail! ${JSON.stringify(task)}`); return null; } for (let index = 0; index < task.files.length; index++) { const file = task.files[index]; if (isNull(file)) { GM_getValue('isDebug') && console.debug(`check aria2 task file fail! ${JSON.stringify(task.files)}`); continue; } try { let videoID = analyzeLocalPath(file?.path)?.filename?.match(/\[([^\[\]]*)\](?=[^\[]*$)/g)?.pop()?.trimHead('[')?.trimTail(']'); if (isNull(videoID) || videoID.isEmpty()) { GM_getValue('isDebug') && console.debug(`check aria2 task videoID fail! ${JSON.stringify(file.path)}`); continue; } return videoID; } catch (error) { continue; } } return null; } async function aria2TaskCheck() { let completed = (await aria2API('aria2.tellStopped', [0, 2048, [ 'gid', 'status', 'files', 'errorCode', 'bittorrent' ]])).result.filter((task) => isNull(task.bittorrent) && (task.status === 'complete' || task.errorCode === '13')).map((task) => aria2TaskExtractVideoID(task)).filter(Boolean).map((i) => i.toLowerCase()); let active = await aria2API('aria2.tellActive', [[ 'gid', 'downloadSpeed', 'files', 'bittorrent' ]]); let needRestart = active.result.filter((i) => isNull(i.bittorrent) && !Number.isNaN(i.downloadSpeed) && Number(i.downloadSpeed) <= 1024); for (let index = 0; index < needRestart.length; index++) { const task = needRestart[index]; let videoID = aria2TaskExtractVideoID(task); if (!isNull(videoID) && !videoID.isEmpty()) { if (!completed.includes(videoID.toLowerCase())) { let cache = (await db.videos.where('ID').equals(videoID).toArray()).pop(); let videoInfo = await (new VideoInfo(cache)).init(videoID); videoInfo.State && await pushDownloadTask(videoInfo); } await aria2API('aria2.forceRemove', [task.gid]); } } } function uninjectCheckbox(element) { if (element instanceof HTMLElement) { if (element instanceof HTMLInputElement && element.classList.contains('selectButton')) { element.hasAttribute('videoID') && pageSelectButtons.delete(element.getAttribute('videoID')); } if (element.querySelector('input.selectButton')) { element.querySelectorAll('.selectButton').forEach(i => i.hasAttribute('videoID') && pageSelectButtons.delete(i.getAttribute('videoID'))); } } } function injectCheckbox(element, compatible) { let ID = element.querySelector('a.videoTeaser__thumbnail').href.toURL().pathname.split('/')[2]; let Name = element.querySelector('.videoTeaser__title')?.getAttribute('title').trim(); let Alias = element.querySelector('a.username')?.getAttribute('title'); let Author = element.querySelector('a.username')?.href.toURL().pathname.split('/').pop(); let node = compatible ? element : element.querySelector('.videoTeaser__thumbnail'); let button = renderNode({ nodeType: 'input', attributes: Object.assign(selectList.has(ID) ? { checked: true } : {}, { type: 'checkbox', videoID: ID, videoName: Name, videoAlias: Alias, videoAuthor: Author }), className: compatible ? ['selectButton', 'selectButtonCompatible'] : 'selectButton', events: { click: (event) => { event.target.checked ? selectList.set(ID, { Title: Name, Alias: Alias, Author: Author }) : selectList.delete(ID); event.stopPropagation(); event.stopImmediatePropagation(); return false; } } }); pageSelectButtons.set(ID, button); originalNodeAppendChild.call(node, button); } function firstRun() { console.log('First run config reset!'); GM_listValues().forEach(i => GM_deleteValue(i)); config = new Config(); editConfig = new configEdit(config); let confirmButton = renderNode({ nodeType: 'button', attributes: { disabled: true, title: i18n[language()].ok }, childs: '%#ok#%', events: { click: () => { GM_setValue('isFirstRun', false); GM_setValue('version', GM_info.script.version); unsafeWindow.document.querySelector('#pluginOverlay').remove(); editConfig.inject(); } } }); originalNodeAppendChild.call(unsafeWindow.document.body, renderNode({ nodeType: 'div', attributes: { id: 'pluginOverlay' }, childs: [ { nodeType: 'div', className: 'main', childs: [ { nodeType: 'p', childs: '%#useHelpForBase#%' }, { nodeType: 'p', childs: '%#useHelpForInjectCheckbox#%' }, { nodeType: 'p', childs: '%#useHelpForCheckDownloadLink#%' }, { nodeType: 'p', childs: i18n[language()].useHelpForManualDownload }, { nodeType: 'p', childs: i18n[language()].useHelpForBugreport } ] }, { nodeType: 'div', className: 'checkbox-container', childs: { nodeType: 'label', className: ['checkbox-label', 'rainbow-text'], childs: [{ nodeType: 'input', className: 'checkbox', attributes: { type: 'checkbox', name: 'agree-checkbox' }, events: { change: (event) => { confirmButton.disabled = !event.target.checked; } } }, '%#alreadyKnowHowToUse#%'] } }, confirmButton ] })); } function pageChange() { GM_getValue('isDebug') && console.debug(pageSelectButtons); } async function main() { if (GM_getValue('isFirstRun', true)) { firstRun(); return; } if (!await config.check()) { newToast(ToastType.Info, { text: `%#configError#%`, duration: 60 * 1000, }).showToast(); editConfig.inject(); return; } GM_setValue('version', GM_info.script.version); if (config.autoInjectCheckbox) { Node.prototype.appendChild = function (node) { if (node instanceof HTMLElement && node.classList.contains('videoTeaser')) { injectCheckbox(node, compatible); } return originalNodeAppendChild.call(this, node); }; } Node.prototype.removeChild = function (child) { uninjectCheckbox(child); return originalRemoveChild.apply(this, [child]); }; Element.prototype.remove = function () { uninjectCheckbox(this); return originalRemove.apply(this); }; new MutationObserver((m, o) => { if (m.some(m => m.type === 'childList' && unsafeWindow.document.getElementById('app'))) { pluginMenu.inject(); o.disconnect(); } }).observe(unsafeWindow.document.body, { childList: true, subtree: true }); originalAddEventListener('mouseover', (event) => { mouseTarget = event.target instanceof Element ? event.target : null; }); unsafeWindow.history.pushState = function (...args) { originalPushState.apply(this, args); pageChange(); }; unsafeWindow.history.replaceState = function (...args) { originalReplaceState.apply(this, args); pageChange(); }; unsafeWindow.document.addEventListener('keydown', function (e) { if (e.code === 'Space' && !isNull(mouseTarget)) { let element = findElement(mouseTarget, '.videoTeaser'); let button = element && (element.matches('.selectButton') ? element : element.querySelector('.selectButton')); button && button.click(); button && e.preventDefault(); } }); let notice = newToast(ToastType.Info, { node: toastNode([ `加载完成`, { nodeType: 'br' }, `公告: `, ...i18n[language()].notice ]), duration: 10000, gravity: 'bottom', position: 'center', onClick() { notice.hideToast(); } }); notice.showToast(); if (config.addUnlistedAndPrivate) { for (let subscribedPage = 0; subscribedPage < 10; subscribedPage++) { (await (await fetch(`https://api.iwara.tv/videos?subscribed=true&limit=50&rating=ecchi&page=${subscribedPage}`, { method: 'GET', headers: await getAuth() })).json()).results.forEach(info => new VideoInfo().init(info.id, info)); } } } if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('3.2.5')) === VersionState.Low) { GM_setValue('isFirstRun', true); alert(i18n[language()].configurationIncompatible); } (unsafeWindow.document.body ? Promise.resolve() : new Promise(resolve => originalAddEventListener.call(unsafeWindow.document, "DOMContentLoaded", resolve))).then(main); })();